package org.ayal.SPT;

/* Copyright 2011 Shai Ayal
 * 
 * This file is part of SPT.
 *
 * SPT is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SPT is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with SPT.  If not, see <http://www.gnu.org/licenses/>.
 */

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.State;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

import com.jcraft.jsch.HostKey;
import com.jcraft.jsch.HostKeyRepository;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

import java.util.Timer;
import java.util.TimerTask;

/**
 *         The service in charge of maintaining a working SSH connection with
 *         port forwarding. It should be robust, and should try to reconnect at
 *         any sign of connection problem.
 * 
 * @author Shai Ayal.
 */

public class Connection extends Service {

	public final static String TAG = "SPT.Connection";

	public enum ConnectionState {
		CONNECTED,      // Connected (including loss of connectivity not related to network change) 
		PENDING,    // Waiting before reconnect after new network has come up
		NO_NETWORK, // No available network
		IDLE,       // Doing nothing -- no ConnectionInfo
		CONNECTING  // Connection process -- makes the checkTimer return immediately
	}

	private ConnectionState state = null;

	private ConnectionInfo ci = null;
	
	private DynamicForwarder df = null;

	// JSch SSH members
	private JSch jsch = null;
	private Session session = null;
	private static final int SOCKET_TIMEOUT = 5000;

	private ConnectivityChangeReceiver ccr = null;

	private static final int CHECK_CONNECTION_DELAY = 5000;
	private Timer checkTimer = null;

	private NotificationManager mNotificationManager = null;
	private static final int sshT_ID = 1;
	
	private CharSequence LastMessgage;


	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Service#onCreate()
	 */
	@Override
	public void onCreate() {
		super.onCreate();
		
		// -----------------------------------------------------------------------------------
		// Initialize state --  NOTE -- this should come first!
		NetworkInfo ni = ((ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
		if (null != ni) {
			if (ni.getState() == State.CONNECTED) {
				state = ConnectionState.IDLE;
			} else {
				state = ConnectionState.NO_NETWORK;
			}
		}

		// -----------------------------------------------------------------------------------
		// Jsch
		jsch = new JSch();

		// -----------------------------------------------------------------------------------
		// Connectivity status
		ccr = new ConnectivityChangeReceiver();
		registerReceiver(ccr, new IntentFilter(
				ConnectivityManager.CONNECTIVITY_ACTION));

		// -----------------------------------------------------------------------------------
		// Notification manager
		String ns = Context.NOTIFICATION_SERVICE;
		mNotificationManager  = (NotificationManager) getSystemService(ns);


		// -----------------------------------------------------------------------------------
		// checkTimer setup
		checkTimer = new Timer();
		checkTimer.scheduleAtFixedRate(new TimerTask() {
			@Override
			public void run() {
				synchronized (state) {
					switch (state) {
					case CONNECTING:
						// Nothing to do, we are in the middle of connection/disconnection 
						break;
					case NO_NETWORK:
						// Nothing to do, Only a new network can get us out of this state ...
						break;
					case IDLE:
						// If we have get ConnectionInfo, let's connect!
						if (ci != null) {
							state = ConnectionState.CONNECTED;
						}
						break;
					case PENDING:
						// next time around we'll try to connect. This introduces a
						// delay to allow the newly connected network to settle down.
						state = ConnectionState.CONNECTED;
						break;
					case CONNECTED:
						if (ci == null) {
							state = ConnectionState.IDLE;
							ConnectivityLost();
						}
						else {
							// reconnect if needed!
							if (session == null || !session.isConnected()) {
								Log.d(TAG, "checkTimer Reconnecting");
								ConnectivityRestored();
							}
						}
						break;
					default:
						Log.d(TAG, "checkTimer: Unknown state " + state.toString());
						break;
					}
				}
				return;
			}
		}, CHECK_CONNECTION_DELAY, CHECK_CONNECTION_DELAY);

	}

	/**
	 * @return the lastMessgage
	 */
	public CharSequence getLastMessgage() {
		if (LastMessgage == null) {
                    return "-";
                }
                else {
                    return LastMessgage;
                }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Service#onStart(android.content.Intent, int)
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		super.onStartCommand(intent, flags, startId);

		return START_STICKY;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Service#onDestroy()
	 */
	@Override
	public void onDestroy() {
		super.onDestroy();

		if (checkTimer != null) {
			checkTimer.cancel();
			checkTimer.purge();
		}

		unregisterReceiver(ccr);
		ConnectivityLost();
		mNotificationManager.cancelAll();
		ci = null;
	}

	public class ConnectivityChangeReceiver extends BroadcastReceiver {
		@Override
		public void onReceive(Context context, Intent intent) {
			NetworkInfo info = ((ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
			synchronized (state) {
				if (info != null  && info.getState() == State.CONNECTED) {
					if (ci == null) {
						state = ConnectionState.IDLE;
					}
					else {
						state = ConnectionState.PENDING;
					}
				} else {
					state = ConnectionState.NO_NETWORK;
					ConnectivityLost();
				}

			}
		}
	}

	private void Notify (int icon, String tickerText, CharSequence contentTitle, CharSequence contentText) {
		long when = System.currentTimeMillis();
		Notification notification = new Notification(icon, tickerText, when);
		notification.flags |= Notification.FLAG_NO_CLEAR;

		Context context = getApplicationContext();
		Intent notificationIntent = new Intent(this, Main.class);
		notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
		notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);
		startForeground(sshT_ID, notification);
	}

	private void ConnectivityLost () {
		synchronized (state) {
			ConnectionState tmp = state;
			state = ConnectionState.CONNECTING;
			LastMessgage = "Disconnected";
			try {
				if (session!= null && session.isConnected()) {
					session.disconnect();
				}
				if (jsch != null) {
					jsch.removeAllIdentity();
				}
				if (df != null) {
					df.stop();
				}
			} catch (Exception e) {
				Notify (R.drawable.disconnected, "Disconnect Failure", "SPT", e.toString());
				Log.e(TAG, "ConnectivityLost: " + e.toString());
				LastMessgage = e.toString ();
			}
			state = tmp;
		}
		Notify (R.drawable.pending, "Disconnected", "SPT", "Disconnected");
	}

	private void ConnectivityRestored () {
		synchronized (this) {
			ConnectionState tmp = state;
			state = ConnectionState.CONNECTING;
			if (null != ci) {
				try {
					Notify (R.drawable.pending, "Connection Pending", "SPT", "Connecion Pending");
					if (ci.getKeypath().length() > 0) {
						jsch.addIdentity(ci.getKeypath());
					}
					jsch.setKnownHosts(getFilesDir() + "/"+ R.string.knownhosts);
					session = jsch.getSession(ci.getUser(), ci.getHost(), ci.getPort());
					session.setUserInfo(ci);
					session.setConfig("HashKnownHosts",  "no");
					if (isHostKnown(ci.getHost())) {
						session.setConfig("StrictHostKeyChecking", "yes");
					}
					if (ci.getCompression()) {
						session.setConfig("compression.s2c", "zlib@openssh.com,zlib,none");
						session.setConfig("compression.c2s", "zlib@openssh.com,zlib,none");
						session.setConfig("compression_level", "3");
					}
					session.connect();
					ci.setPortForwardingL(session);

					if (ci.getDynamic_port() > 0 ) {
						if (df != null) {
							df.stop();
						}
						df = new DynamicForwarder(ci.getDynamic_port(), session);
					}

					session.setServerAliveInterval(SOCKET_TIMEOUT);
					session.setServerAliveCountMax(10);
					Notify (R.drawable.connected, "Connected", "SPT", "Connected to " + ci.getHost() + ":" + ci.getPort());
                                        LastMessgage = "Connected to " + ci.getHost() + ":" + ci.getPort();
				} catch (Exception e) {
					if (session != null) {
						session.disconnect();
					}
					if (df != null) {
						df.stop ();
					}
					Notify (R.drawable.disconnected, "Connection Failure", "SPT", e.toString());
					Log.e(TAG, "ConnectivityRestored: " + e.toString());
                                        LastMessgage = e.toString ();
				} 
			}
			state = tmp;
		}
	}

	private Boolean isHostKnown (String host) {
		try {
			jsch.setKnownHosts(getFilesDir() + "/"+ R.string.knownhosts);
			HostKeyRepository hkr = jsch.getHostKeyRepository();
			HostKey[] hks=hkr.getHostKey();
			if(hks!=null) {
                for (HostKey hk : hks)
                    if (hk.getHost().compareToIgnoreCase(host) == 0) {
                        return true;
                    }
            }
		} catch (Exception e) {
			Log.e(TAG, e.toString());
		}
		return false;
	}

	// -----------------------------------------------------------------------------------
	// Binding
	public class LocalBinder extends Binder {
		Connection getService() {
			return Connection.this;
		}
	}

	private final IBinder mBinder = new LocalBinder();

	@Override
	public IBinder onBind(Intent arg0) {
		return mBinder;
	}

	public void setConnectionInfo (ConnectionInfo ci) {
		this.ci = ci;
	}

	public ConnectionState getState () {
		return state;
	}
}
